API Design Patterns — 16장 다형성 딥다이브 학습 노트

출처: API Design Patterns (JJ Geewax) | 참고: https://google.aip.dev/


16장에서 다루는 내용

  • 다형성이란 — OOP의 다형성을 API에 적용
  • 다형성의 장점과 API에서의 용례 — 중복 메서드 방지, 여러 타입 함께 나열
  • 다형성 리소스를 구성하는 방법 — type 필드, 데이터 구조, 데이터 검증
  • 리소스 지향 API에서 다형성 메서드를 피해야 하는 이유 — 동작 분화 위험

전체 흐름도

[OOP 다형성]                      [API 다형성]                    [구현 세부사항]
    |                                |                              |
    v                                v                              v
+------------------+     +---------------------+     +---------------------------+
| interface Shape  |     | 다형성 리소스          |     | 16.3.2 다형성 구조         |
| Triangle, Square |---->| = 하나의 제네릭        |---->| - type 필드 (문자열)       |
| 가 구현           |     | 리소스로 여러 타입     |     | - 데이터 구조 (슈퍼셋)     |
+------------------+     | 을 표현               |     | - 데이터 검증              |
                         +---------------------+     +---------------------------+
                           |                                   |
                           v                          +---------------------------+
                    +-----------+                     | 16.3.3 다형성 동작         |
                    | ListShape |                     | 기존 메서드와 동일하게 동작 |
                    | 하나로    |                     +---------------------------+
                    | 모든 타입 |                              |
                    | 나열 가능 |                     +---------------------------+
                    +-----------+                     | 16.3.4 다형성 메서드       |
                                                      | → 피해야 함!             |
                                                      | DeleteResource() 위험    |
                                                      +---------------------------+

선수 지식 체크리스트

  • [ ] REST API 기본 개념 (리소스, 엔드포인트, HTTP 메서드)
  • [ ] 표준 메서드 (Get, Create, Update, Delete, List)
  • [ ] OOP 기초 (인터페이스, 상속, 다형성 개념)

핵심 키워드

키워드 의미
다형성 하나의 공통 인터페이스를 여러 타입에 적용하는 개념
다형성 리소스 type 필드로 세부 타입을 구분하는 제네릭 리소스
다형성 메서드 여러 리소스 타입에서 동작하는 제네릭 메서드 (비권장)
type 필드 리소스의 세부 타입을 명시하는 문자열 필드
슈퍼셋 모든 서브타입의 필드를 하나의 리소스에 모아놓은 구조
동작 분화 처음엔 같았던 동작이 시간이 지나며 타입별로 달라지는 현상

16.1 서론 — OOP의 다형성을 API에

한 줄 요약

객체지향 프로그래밍의 강력한 다형성을 리소스 지향 API에 접목한다!

쉬운 설명 (메시지 앱 비유)

카카오톡에서 보내는 메시지를 생각해보자: - 텍스트 메시지: "안녕하세요" (글자) - 사진 메시지: 사진 파일 - 동영상 메시지: 동영상 파일 - 음성 메시지: 녹음 파일

이것들은 모두 "메시지"라는 공통점이 있지만, 담는 내용이 다르다.

# 다형성 없이 — 메서드가 타입마다 필요
ListTextMessages()      → 텍스트만
ListPhotoMessages()     → 사진만
ListVideoMessages()     → 동영상만
ListAudioMessages()     → 음성만
# 채팅방 전체 메시지를 보려면 4번 호출! 😱

# 다형성 사용 — 하나의 메서드로 모든 타입
ListMessages()          → 모든 타입의 메시지 한 번에! ✅

실무 예제 — 다형성이 필요한 실제 시스템들

1. 메시징 앱 (카카오톡, Slack)
   Message: text, image, video, audio, file, location, sticker
   → ListMessages로 채팅 내역을 시간순 통합 나열

2. 결제 시스템
   PaymentMethod: credit_card, bank_transfer, mobile_pay, crypto
   → ListPaymentMethods로 사용자의 모든 결제 수단 나열

3. 알림 시스템
   Notification: email, push, sms, in_app
   → ListNotifications로 모든 타입의 알림을 통합 나열

4. CI/CD 파이프라인
   Step: build, test, deploy, approval, notification
   → ListSteps로 파이프라인의 모든 단계를 순서대로 나열

5. CMS (콘텐츠 관리)
   Block: text, image, video, code, quote, table
   → ListBlocks로 페이지의 모든 콘텐츠 블록을 순서대로 나열

TypeScript 다형성 예시

// 공통 인터페이스
interface Shape {
    countSides(): number;
}

// 구현체 1
class Triangle implements Shape {
    countSides() { return 3; }
}

// 구현체 2
class Square implements Shape {
    countSides() { return 4; }
}

// 다형성 활용 — Triangle인지 Square인지 신경 안 써도 됨!
function countSides(shape: Shape): number {
    return shape.countSides();
}

16.2 개괄 — 다형성 리소스의 개념

한 줄 요약

다형성 리소스는 제네릭 인터페이스(Message)의 형태를 취하며, type 필드로 세부 타입(text/photo/audio/video)을 특정한다.

장점

  1. 중복 방지: ListTextMessages, ListPhotoMessages 대신 ListMessages 하나로 충분
  2. 확장성: 새로운 타입 추가 시 메서드를 추가할 필요 없음
  3. 통합 목록: 채팅방의 모든 메시지를 한 번에 시간순으로 나열 가능

핵심 체크포인트

  • ✅ 여러 타입을 함께 나열해야 할 때 다형성 리소스 사용
  • ✅ type 필드로 서브타입을 구분하고 서버에서 검증
  • ✅ 모든 표준 메서드가 동일하게 적용됨

16.3 구현

16.3.1 다형성 리소스 사용 여부 결정

의사결정 트리

Q1: 여러 타입의 리소스를 함께 나열해야 하는가?
├── NO → 독립 리소스 사용 (다형성 불필요)
│
└── YES → Q2: 각 타입이 "제네릭 리소스의 특수한 타입"인가?
           ├── NO → 독립 리소스 사용
           │         (예: ChatRoom과 Broadcast는 근본적으로 다름)
           │
           └── YES → Q3: 타입 간 동작(메서드)이 동일한가?
                      ├── YES → 다형성 리소스 사용 ✅
                      └── 약간 다름 → 다형성 리소스 사용 가능
                                      (type 기반 분기로 처리)

사용해야 하는 경우 — 상세

1. 채팅 앱
   텍스트, 사진, 동영상, 음성 메시지를 시간순으로 함께 나열
   → PhotoMessage는 Message의 특수한 타입인가? YES ✅
   → ListMessages 하나로 모든 타입 나열

2. 결제 수단
   신용카드, 계좌이체, 모바일페이를 사용자 결제 수단으로 나열
   → CreditCard는 PaymentMethod의 특수한 타입인가? YES ✅
   → ListPaymentMethods 하나로 모든 결제 수단 나열

3. CMS 블록
   텍스트, 이미지, 코드, 인용구를 페이지 순서대로 나열
   → ImageBlock은 Block의 특수한 타입인가? YES ✅
   → ListBlocks 하나로 모든 콘텐츠 블록 나열

사용하지 않는 것이 나은 경우 — 상세

1. 채팅방 vs 공지 그룹
   ChatRoom: 양방향, 모든 멤버가 메시지 가능
   Broadcast: 단방향, 관리자만 발신, 구독/해지 모델
   → Broadcast는 ChatRoom의 특수한 타입인가? NO ❌
   → 동작이 근본적으로 다름 → 별도 리소스

2. 파일 vs 폴더
   File: 데이터를 저장, 다운로드 가능
   Folder: 다른 항목을 포함하는 컨테이너
   → Folder는 File의 특수한 타입인가? NO ❌
   → 역할이 근본적으로 다름 → 별도 리소스

3. 사용자 vs 서비스 계정
   User: 로그인, 프로필 수정, 비밀번호 변경
   ServiceAccount: API 키 기반, 봇 동작, 비밀번호 없음
   → 인증/권한 모델이 완전히 다름 → 별도 리소스

16.3.2 다형성 구조

type 필드 정의

interface Message {
    id: string;
    sender: string;
    type: 'text' | 'photo' | 'audio' | 'video';   // ← 문자열 타입 필드
}

왜 문자열인가? 열거형(enum)은 추가/삭제 시 기존 클라이언트 코드를 깨뜨릴 수 있다. 문자열이면 새 타입을 추가해도 기존 코드에 영향이 없다.

type은 불변: 한번 생성된 리소스의 타입을 바꾸면 다른 리소스가 이 리소스를 참조하는 전제가 깨진다. 비디오 메시지를 텍스트로 바꾸면 혼란 발생!

데이터 구조 — 3가지 방식

// 방식 1: 슈퍼셋 — 모든 타입의 필드를 포함
interface Message {
    id: string;
    sender: string;
    type: 'text' | 'photo' | 'audio' | 'video';
    text?: string;           // text 타입일 때만 사용
    photoUri?: string;       // photo 타입일 때만 사용
    videoUri?: string;       // video 타입일 때만 사용
    audioUri?: string;       // audio 타입일 때만 사용
}

// 방식 2: 단일 content 필드 — 타입에 따라 다른 의미
interface Message {
    id: string;
    sender: string;
    type: 'text' | 'photo' | 'audio' | 'video';
    content: string;         // text일 때는 텍스트, photo일 때는 URI
}

// 방식 3: content 필드 + Media 인터페이스 (권장)
interface Message {
    id: string;
    sender: string;
    type: 'text' | 'image' | 'audio' | 'video';
    content: string | Media;  // text → string, 나머지 → Media
}

interface Media {
    contentUri: string;
    contentType: string;      // "video/mp4" 등
}

도형 예제 — 차원 데이터가 타입별로 다른 경우

interface Shape {
    id: string;
    type: 'square' | 'circle' | 'triangle';
    dimension: SquareDimension | CircleDimension | TriangleDimension;
}

interface SquareDimension { length: number; }
interface CircleDimension { radius: number; }
interface TriangleDimension { base: number; height: number; }

데이터 검증

# 정사각형인데 반지름 값을 보내면?
CreateShape({
    type: 'square',
    dimension: { radius: 10, length: 10 }
})
→ radius는 무시! (에러 반환보다 필요한 것만 확인 후 나머지 배제)

# 사진 메시지인데 content가 유효한 URI가 아니면?
CreateMessage({ type: 'photo', content: '그냥 텍스트' })
→ 에러 반환 (photo 타입이면 content는 유효한 URI여야 함)

원칙: 유효하지 않은 입력 데이터는 에러를 반환하는 대신, 필요한 데이터가 존재하는지 확인한 후 불필요한 데이터는 무시한다.

16.3.3 다형성 동작

// API 메서드 — 기존 표준/사용자 메서드와 동일하게 동작
abstract class ShapeApi {
    @post("/{id=shapes/*}:countSides")
    CountShapeSides(req: CountShapeSidesRequest): CountShapeSidesResponse;
}

// 호출 결과 — 타입에 따라 다른 결과
CountShapeSides({ id: "shapes/square-1" })  → { sides: 4 }
CountShapeSides({ id: "shapes/triangle-1" }) → { sides: 3 }

16.3.4 왜 다형성 메서드를 사용하지 않는가?

다형성 메서드의 유혹

// 모든 리소스 타입에서 동작하는 제네릭 삭제
abstract class GenericApi {
    @delete("/{id=*/*}")                    // ← 모든 리소스 타입에 매핑
    DeleteResource(req: DeleteResourceRequest): void;
}

"이게 훨씬 간단하잖아! 왜 안 쓰지?"

위험한 이유 2가지

1. 리소스 타입별로 동작이 달라질 수 있음

DeleteChatRoom()    → 논리 삭제 (25장) + 검증 요청 (27장) 수행
DeleteMessage()     → 즉시 물리 삭제

# 하나의 DeleteResource()로 합치면?
# 동작을 변경하면 모든 리소스에 영향!

2. 동작 변경이 모든 리소스에 전파됨

# ChatRoom 삭제에 검증을 추가하고 싶은데...
DeleteResource()를 수정하면 Message 삭제에도 검증이 적용됨!
→ 의도하지 않은 부작용

원칙: 다형성 리소스에 집중하고, 다형성 메서드는 피하라.

16.3.5 최종 API 정의

abstract class ChatRoomApi {
    static version = "v1";
    static title = "Chat Room API";

    // === ChatRoom 표준 메서드 ===
    @post("/chatRooms")
    CreateChatRoom(req: CreateChatRoomRequest): ChatRoom;

    @get("/{id=chatRooms/*}")
    GetChatRoom(req: GetChatRoomRequest): ChatRoom;

    @get("/chatRooms")
    ListChatRooms(req: ListChatRoomsRequest): ListChatRoomsResponse;

    // === Message 표준 메서드 (다형성 리소스) ===
    @post("/{parent=chatRooms/*}/messages")
    CreateMessage(req: CreateMessageRequest): Message;

    @get("/{id=chatRooms/*/messages/*}")
    GetMessage(req: GetMessageRequest): Message;

    @get("/{parent=chatRooms/*}/messages")
    ListMessages(req: ListMessagesRequest): ListMessagesResponse;

    @delete("/{id=chatRooms/*/messages/*}")
    DeleteMessage(req: DeleteMessageRequest): void;
}

interface ChatRoom {
    id: string;            // "chatRooms/room-1"
    name: string;
    memberCount: number;
}

interface CreateMessageRequest {
    parent: string;        // "chatRooms/room-1"
    resource: Message;
}

// === 다형성 리소스: type으로 서브타입 결정 ===
interface Message {
    id: string;            // "chatRooms/room-1/messages/msg-42"
    sender: string;        // "users/alice"
    type: 'text' | 'image' | 'audio' | 'video';  // 불변 문자열
    content: string | Media;  // type에 따라 다른 형태
    createdAt: DateTime;
}

interface Media {
    uri: string;           // "https://storage.example.com/photo-123.jpg"
    contentType: string;   // "image/jpeg", "video/mp4", "audio/ogg"
    sizeBytes: number;     // 파일 크기
}

실제 API 호출 예시

// === 텍스트 메시지 생성 ===
POST /chatRooms/room-1/messages
{
    sender: "users/alice",
    type: "text",
    content: "안녕하세요! 오늘 회의 시간이 언제인가요?"
}
→ 201 Created
{
    id: "chatRooms/room-1/messages/msg-42",
    sender: "users/alice",
    type: "text",
    content: "안녕하세요! 오늘 회의 시간이 언제인가요?",
    createdAt: "2024-03-15T09:00:00Z"
}

// === 이미지 메시지 생성 ===
POST /chatRooms/room-1/messages
{
    sender: "users/bob",
    type: "image",
    content: {
        uri: "https://storage.example.com/meeting-schedule.png",
        contentType: "image/png",
        sizeBytes: 245760
    }
}
→ 201 Created { id: "msg-43", type: "image", ... }

// === 모든 타입의 메시지를 한 번에 나열 ===
GET /chatRooms/room-1/messages
→ 200 OK
{
    messages: [
        { id: "msg-42", type: "text", content: "안녕하세요!...", ... },
        { id: "msg-43", type: "image", content: { uri: "...", ... }, ... }
    ]
}

// === 특정 타입만 필터링 (22장 필터링 패턴) ===
GET /chatRooms/room-1/messages?filter=type='image'
→ 이미지 메시지만 반환

16.4 트레이드오프

한 줄 요약

다형성은 리소스를 특정 표현에 묶어놓게 된다. 비슷해 보이는 리소스도 근본적 차이가 존재할 수 있다.

  • 리소스 결합: 다형성으로 묶인 리소스는 나중에 분리하기 어려울 수 있음
  • 경험적 방법: 각 리소스가 제네릭 리소스의 특수한 타입인지 확인 (PhotoMessage는 Message의 특수 타입인가? ✅)

16.4.1 분리가 어려운 이유

처음: Message로 text, image, video를 통합 관리
→ 시간이 지남...
→ video 메시지에만 "트랜스코딩 상태", "스트리밍 URL" 필드가 필요
→ text 메시지에만 "서식(bold, italic)", "링크 미리보기" 필드가 필요
→ Message 인터페이스가 비대해짐

# 분리하려면?
# 기존: ListMessages → Message[] (모든 타입)
# 변경: ListTextMessages → TextMessage[], ListVideoMessages → VideoMessage[]
# → 기존 클라이언트 코드가 전부 깨짐! (breaking change)

16.4.2 새로운 타입 추가 시 주의사항

# 기존 타입: text, image, audio, video
# 새 타입 추가: sticker

# 서버 측: type: 'sticker' 추가 + content 구조 정의 → 간단!
# 클라이언트 측: "알 수 없는 타입"을 처리할 수 있어야 함

// 좋은 클라이언트 코드
switch (message.type) {
    case 'text': renderText(message); break;
    case 'image': renderImage(message); break;
    default: renderGeneric(message);     // ← 미지원 타입 처리!
}

// 나쁜 클라이언트 코드
if (message.type === 'text') { ... }
else if (message.type === 'image') { ... }
// else → 아무것도 안 함 → sticker 메시지가 안 보임!

16.5 연습문제 풀이

Q1: 결제 시스템에서 CreditCard, BankTransfer, MobilePay를 다형성 리소스로 관리할 때 type 필드와 데이터 구조를 설계하라.

A: ```typescript interface PaymentMethod { id: string; type: 'credit_card' | 'bank_transfer' | 'mobile_pay'; displayName: string; // "신한카드 ***1234" details: CreditCardDetails | BankTransferDetails | MobilePayDetails; }

interface CreditCardDetails { last4: string; brand: string; expMonth: number; expYear: number; } interface BankTransferDetails { bankName: string; accountNumber: string; } interface MobilePayDetails { provider: string; phoneNumber: string; } ```

Q2: 폴더와 파일을 하나의 다형성 리소스(Item)로 관리할 수 있는가?

A: 부적합하다. 폴더는 다른 항목을 포함하는 컨테이너이고 파일은 데이터를 저장하는 단위이다. 폴더에는 "하위 항목 추가/제거" 같은 메서드가 필요하지만 파일에는 불필요하다. 동작이 근본적으로 다르므로 별도 리소스가 적합.

Q3: 다형성 메서드(DeleteResource)가 안전한 경우가 있는가?

A: 모든 리소스 타입이 정확히 동일한 방식으로 삭제되고, 미래에도 분화되지 않을 것이 확실한 경우에만. 하지만 이런 확신은 거의 불가능하므로 실무에서는 항상 리소스별 삭제 메서드를 분리하는 것이 안전하다.


부록 A: 용어 사전

용어 영문 의미
다형성 Polymorphism 하나의 공통 인터페이스를 여러 타입에 적용하는 개념
다형성 리소스 Polymorphic Resource type 필드로 세부 타입을 구분하는 제네릭 리소스
다형성 메서드 Polymorphic Method 여러 리소스 타입에서 동작하는 제네릭 메서드 (비권장)
슈퍼셋 Superset 모든 서브타입의 필드를 포함하는 통합 인터페이스
불변 타입 Immutable Type 생성 후 변경할 수 없는 type 필드

부록 B: 핵심 비교표

항목 독립 리소스 (타입별) 다형성 리소스
메서드 수 타입 수 × 메서드 수 1세트
통합 나열 불가 (각각 호출) ListResource 하나
타입별 동작 독립적 제어 가능 공통 동작만
확장성 새 타입 = 새 메서드 세트 새 타입 = type 값 추가
복잡도 타입 수에 비례 일정

부록 C: 추천 참고 자료 & 링크

자료 설명
5장 데이터 타입과 기본값 열거형 대신 문자열을 사용하는 이유
22장 필터링 type 필드로 특정 타입만 나열하기
25장 논리 삭제 다형성 메서드가 위험한 이유의 예시
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플